Skip to content

Pick up JasperFx.Events 1.30.0 + pre-warm EventGraph._nameToType#4296

Merged
jeremydmiller merged 1 commit intomasterfrom
marten-cold-start-eventtype-prewarm
Apr 27, 2026
Merged

Pick up JasperFx.Events 1.30.0 + pre-warm EventGraph._nameToType#4296
jeremydmiller merged 1 commit intomasterfrom
marten-cold-start-eventtype-prewarm

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Continues the 8.x cold-start trim from #4295. Two more non-breaking optimizations from the umbrella issue (#4294).

What lands

  1. JasperFx.Events bumped 1.29.1 → 1.30.0, picking up JasperFx/jasperfx#193. ProjectionGraph.DiscoverGeneratedEvolvers now (a) skips framework / library assemblies (System.*, Microsoft.*, JasperFx.*, Marten.*, Wolverine.*, Weasel.*, Npgsql.*, etc.) before reading custom attributes — they structurally cannot carry user-defined [GeneratedEvolverAttribute] — and (b) caches per-assembly results in a process-wide ConcurrentDictionary, so multi-store hosts share the work.

  2. EventGraph._nameToType pre-warmed in Initialize. TypeForDotNetName (src/Marten/Events/EventGraph.cs:551) otherwise falls through to Type.GetType(assemblyQualifiedName) on first read of each event type from the database, and that fallback is itself O(loaded-assemblies). Pre-populating both the AssemblyQualifiedName and FullName keys for every registered event type — in a single ImHashMap.Swap so we don't churn the map — means the first read of every known event type lands directly in the cache. Lazily-registered types (those discovered via _events.OnMissing) still fall through the existing path; behavior unchanged.

Test plan

  • Build clean (Marten + EventSourcingTests)
  • end_to_end_event_capture_and_fetching_the_stream + archiving_events + quick_append_event_capture_and_fetching_the_stream — 208 passed, 0 failed
  • Cross-section that exercises event-type discovery / upcasting / multi-store wiring (Bug_4197, Bug_4277, SchemaChange.Upcasters, Aggregation.global_tenanted_streams) — 59 passed, 0 failed

What's still on the 9.0 board

The bigger items on the umbrella issue — lazy BuildAllMappings, lazy EventGraph.FeatureSchema.Objects, Static-mode AssertValidity skip, IEnumerable<StreamAction> widening, GenerationRules caching with mutation isolation, and anything depending on JasperFx #190 (ITypeLoader) or #191 (CloseAndBuildAs rework) — are still 9.0-scope.

🤖 Generated with Claude Code

JasperFx.Events 1.30.0 (JasperFx/jasperfx#193) makes
ProjectionGraph.DiscoverGeneratedEvolvers cheaper at cold start by
filtering framework / library assemblies out before the
GetCustomAttributes scan and by caching results across IDocumentStore
instances in the same process. Bumps the pin so we benefit immediately.

Companion Marten-side optimization in the same PR: pre-populate
EventGraph._nameToType from registered event types during Initialize.
TypeForDotNetName otherwise falls through Type.GetType(assemblyQualifiedName)
on first read of each event type from the database, and that fallback is
itself O(loaded-assemblies). Pre-warming both the AssemblyQualifiedName and
FullName entries in a single ImHashMap.Swap means the first read of every
known event type lands directly in the cache. New types (those discovered
lazily via _events.OnMissing) still fall through the existing path.

Continues the 8.x cold-start trim begun in #4295. See umbrella issue #4294.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 5c1f6ae into master Apr 27, 2026
6 checks passed
@jeremydmiller jeremydmiller deleted the marten-cold-start-eventtype-prewarm branch April 27, 2026 23:58
jeremydmiller added a commit that referenced this pull request Apr 28, 2026
…ze (#4302)

Round 3 of the 8.x cold-start trim begun in #4295/#4296. Two more lazy
caches in EventGraph that paid an O(n) linear walk on first lookup of
each name; pre-populate them at Initialize from already-known data so
the first request of every alias is a hit.

* _byEventName is keyed by EventTypeName and OnMissing was a
  AllEvents().FirstOrDefault(x => x.EventTypeName == name) walk per miss.
  Add Fill() for every registered mapping in the same loop that already
  walks _events. First lookup of each alias is now O(1).

* _aggregateTypeByName is keyed by aggregate alias and findAggregateType
  iterated Options.Projections.AllAggregateTypes() on miss. Walk that
  collection once at Initialize and Fill the entries up front. Same shape
  as the existing OnMissing logic, just moved off the request path.

Behavior unchanged: in both caches, the existing OnMissing/Fill paths
remain intact for any name that wasn't registered at Initialize time
(rare but possible -- e.g. types added via _events.OnMissing later).

Continues the 8.x cold-start trim. The remaining items are 9.0 scope and
each tracked separately under the Marten 9.0 milestone.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request Apr 28, 2026
Three independent root causes feed the test-flake pattern that's been
hitting recent PRs (#4279, #4281, #4292, #4295, #4296, #4302). All three
are localized; this PR addresses each.

## Root cause 1: shared static random sequence

Target.cs defines `private static readonly Random _random = new Random(67)`
that is consumed by ~80 tests across LinqTests + DocumentDbTests. Each
Target.Random() / GenerateRandomData() call advances the shared sequence,
so a test's effective random data depends on which sibling tests ran
before it. xUnit discovery order is mostly stable but NOT guaranteed
identical run-to-run, especially across CI workers with different load,
.NET TFM combinations, etc. A small order shift consumes a different
slice of the sequence and produces different test data — silently flipping
assertions that depend on exact counts or distributions.

Fix: introduce `Target.ResetRandomSeed(int seed = 67)` so a test that
genuinely depends on specific random data can pin the sequence at the
start. Remove the readonly modifier on _random to allow rebinding.

Updated tests to call ResetRandomSeed():
- Bug_605_unary_expressions_in_where_clause_of_compiled_query (3 facts)
- Bug_3337_select_page.try_it_out
- query_against_child_collections.buildUpTargetData (covers
  can_query_on_enum_properties and many more)

Also tightened Bug_605's assertion: it was hardcoded to `.ShouldBe(15)`
but the real point of the test is "compiled query == inline LINQ query for
the same expression"; the page size of 15 is incidental. Compare against
expected.Count instead so the test is robust to data variance.

## Root cause 2: DateTimeOffset.UtcNow inside a shared LINQ expression

`child_collection_queries.cs:67` was registering this where-clause for the
acceptance suite:

    @where(x => x.Children.Any(c => c.NullableDateOffset <= DateTimeOffset.UtcNow));

That expression runs in BOTH the in-memory LINQ-to-objects "expected"
provider AND the LINQ-to-SQL "actual" provider. Each provider evaluates
DateTimeOffset.UtcNow at its own moment. Target.NullableDateOffset values
are ±60 seconds of "now" from random data; values within microseconds of
either provider's "now" can land on opposite sides of <= and disagree.

Fix: capture a fixed `asOf = DateTimeOffset.UtcNow.AddDays(1)` in the
static ctor and use that as the boundary. The expression now embeds a
constant timestamp that both providers see identically. AddDays(1) puts
it well beyond the test data range so the predicate is meaningfully true
for matching rows.

## Root cause 3: ordering assumptions on server-generated Guids

Bug_4282 asserted `ids.ShouldHaveTheSameElementsAs(doc1.Id, doc3.Id)`
after `OrderBy(x => x.Id)`. The IDs are server-generated Guids; their
sort order does not in general match declaration order (Marten uses
sequential Guids in many configs but not always, depending on the
StoreOptions in scope and the underlying provider). Switched to
set-membership: `Count == 2` plus ShouldContain for each expected id.

## Root cause 4 (defensive): ShouldBeEqualWithDbPrecision tolerance

The helper used to round both sides to 100µs with truncation (`Ticks /
1000 * 1000`) and then ShouldBe. The math works in the common case, but
the assertion was fragile under loaded-runner clock-comparison edge
cases. Switched to a 1ms tolerance check; widely above the worst-case
PostgreSQL truncation (9 ticks ≈ 0.9µs) but still tight enough to catch
real semantic differences. Also produces a clearer failure message when
it does fire.

## Verification

Stress-ran the previously-flaky suites locally: 5x consecutive runs of
all 178 LinqTests.Bugs tests, no failures. All 123 tests across Bug_605,
Bug_4282, Bug_3337, query_against_child_collections, and
child_collection_queries pass. Bug_2283 in DocumentDbTests passes.

Closes #4310.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant